﻿using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Ping.Core
{

	/// <summary>
	/// Implements the channel interface by using a socket to send & receive data-packets.
	/// </summary>
	public sealed class Channel
		: IChannel
	{
		/// <summary>
		/// The result of an async. operation as seen by a user of this class.
		/// Sending & receiving a data-packet is represented by one OuterResult, even if the internal
		/// implementation uses multiple async. operations.
		/// </summary>
		private class OuterResult
			: IAsyncResult
		{
			private readonly AsyncCallback _callback;
			private readonly object _userData;

			private Data _data;
			private bool _completed;
			private Exception _exception;

			public OuterResult(AsyncCallback callback, Data data = null, object userData = null)
			{
				_callback = callback;
				_data = data;
				_userData = userData;
			}

			public Exception Exception
			{
				get { return _exception; }
			}

			public Data Data
			{
				get { return _data; }
			}

			#region IAsyncResult Members

			public bool IsCompleted
			{
				get { return _completed; }
			}

			public WaitHandle AsyncWaitHandle
			{
				get { throw new NotImplementedException(); }
			}

			public object AsyncState
			{
				get { return _userData; }
			}

			public bool CompletedSynchronously
			{
				get { return false; }
			}

			#endregion

			public void Complete(Exception e)
			{
				if (_completed)
					throw new InvalidOperationException("The operation has already been completed");

				_completed = true;
				_exception = e;
				_callback(this);
			}

			public void Complete(Data data)
			{
				if (_completed)
					throw new InvalidOperationException("The operation has already been completed");

				_completed = true;
				_data = data;
				_callback(this);
			}

			public void Complete()
			{
				if (_completed)
					throw new InvalidOperationException("The operation has already been completed");

				_completed = true;
				_callback(this);
			}
		}

		/// <summary>
		/// The result of an inner async. operation...
		/// For example, sending a data-packet consists of sending its length and then the data itself.
		/// EACH of those operations is represented by *one* InnerResult.
		/// </summary>
		private class InnerResult
			: IAsyncResult
		{
			private readonly AsyncCallback _callback;
			private readonly OuterResult _finalResult;
			private readonly byte[] _buffer;
			private bool _completed;
			private Exception _exception;

			public InnerResult(AsyncCallback cb, OuterResult finalResult, byte[] buffer)
			{
				if (cb == null || finalResult == null || buffer == null)
					throw new ArgumentNullException();

				_callback = cb;
				_buffer = buffer;
				_finalResult = finalResult;
			}

			public Exception Exception
			{
				get { return _exception; }
			}

			public byte[] Buffer
			{
				get
				{
					if (_buffer == null)
						throw new InvalidOperationException("No buffer set");

					return _buffer;
				}
			}

			#region IAsyncResult Members

			public bool IsCompleted
			{
				get { return _completed; }
			}

			public WaitHandle AsyncWaitHandle
			{
				get { throw new NotImplementedException(); }
			}

			public object AsyncState
			{
				get { throw new NotImplementedException(); }
			}

			public bool CompletedSynchronously
			{
				get { return false; }
			}

			#endregion

			/// <summary>
			/// Completes an internal socket-operation.
			/// Invokes the async. callback and also invokes the final result in case
			/// the callback throws *any* exception.
			/// </summary>
			/// <param name="exception"></param>
			public void Complete(Exception exception)
			{
				if (_completed)
					throw new InvalidOperationException("This operation has already been completed");

				_exception = exception;
				_completed = true;

				try
				{
					_callback(this);
				}
				catch (Exception e)
				{
					if (!_finalResult.IsCompleted)
						_finalResult.Complete(e);

					// TODO: This error MUST be logged...
				}
			}
		}

		private Socket _socket;

		public bool IsConnected
		{
			get
			{
				// WP7 blatantly lies about this fact... but the remote endpoint is null in
				// case the socket is truly not connected
				return _socket != null && _socket.Connected && _socket.RemoteEndPoint != null;
			}
		}

		public Channel(Socket socket)
		{
			if (socket == null)
				throw new ArgumentNullException();
			if (!socket.Connected)
				throw new ArgumentException();

			_socket = socket;
		}

		public Channel()
		{}

		public IAsyncResult BeginConnect(EndPoint endPoint, object userData, AsyncCallback callback)
		{
			var result = new OuterResult(callback);

			var args = new SocketAsyncEventArgs
			           	{
			           		RemoteEndPoint = endPoint,
							SocketError = SocketError.HostDown,
			           	};
			args.Completed += (s, e) =>
			                  	{
			                  		Exception exception = null;

									// WP7 is a fucking piece of shit: It simply lies about being connected:
									// Whoever is responsible should be brought to justice....

									if (e.SocketError != SocketError.Success)
										exception = new SocketException(e.SocketError);

			                  		result.Complete(exception);
			                  	};

			if (_socket == null)
				_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

			_socket.ConnectAsync(args);

			return result;
		}

		public void EndConnect(IAsyncResult result)
		{
			var e = ((OuterResult)result).Exception;
			if (e != null)
				throw e;
		}

		public void Disconnect()
		{
			_socket.Close();
			_socket.Dispose();
			_socket = null;
		}

		public IAsyncResult BeginSend(Data data, object userData, AsyncCallback callback)
		{
			if (_socket == null)
				throw new InvalidOperationException("The channel is not connected to an endpoint");

			byte[] buffer, length;
			Serialize(data, out buffer, out length);

			var finalResult = new OuterResult(callback, null, userData);

			// Step #1: Send Header
			BeginSendData(length, finalResult, headerResult =>
			{
				// Finished sending header
				EndSendData(headerResult);

				// Step #2: Send Data
				BeginSendData(buffer, finalResult, dataResult =>
				{
					// Finished sending data
					EndSendData(dataResult);

					// In case the operation was successful:
					// We can now invoke the callback :)
					finalResult.Complete();
				});
			});

			return finalResult;
		}

		public void EndSend(IAsyncResult result)
		{
			if (!result.IsCompleted)
				throw new NotImplementedException("Waiting on an incomplete async. operation is not yet implemented");

			if (_socket == null)
				throw new InvalidOperationException("The channel is not connected to an endpoint");

			var e = ((OuterResult)result).Exception;
			if (e != null)
				throw e;
		}



		public IAsyncResult BeginReceive(object userData, AsyncCallback callback)
		{
			if (_socket == null)
				throw new InvalidOperationException("The channel is not connected to an endpoint");

			var final = new OuterResult(callback, null, userData);

			BeginReceiveData(4, final, result1 =>
			                           	{
			                           		var lengthBuffer = EndReceiveData(result1);
			                           		int length = ReadLength(lengthBuffer);

											if (length <= 0)
												throw new ReadRequestException(string.Format("Invalid data size specified: {0}", length));

											BeginReceiveData(length, final, result2 =>
											                                	{
											                                		var dataBuffer = EndReceiveData(result2);
											                                		var data = Packet.Deserialize(new MemoryStream(dataBuffer), dataBuffer.Length);
											                                		final.Complete(data);
											                                	});
			                           	});

			return final;
		}

		public Data EndReceive(IAsyncResult result)
		{
			if (!result.IsCompleted)
				throw new NotImplementedException("Waiting on an incomplete async. operation is not yet implemented");

			if (_socket == null)
				throw new InvalidOperationException("The channel is not connected to an endpoint");

			var outer = (OuterResult) result;
			var e = outer.Exception;
			if (e != null)
				throw e;

			return outer.Data;
		}



		private void BeginSendData(byte[] data, OuterResult finalResult, AsyncCallback callback)
		{
			var op = new InnerResult(callback, finalResult, data);

			var args = new SocketAsyncEventArgs();
			args.SetBuffer(data, 0, data.Length);
			args.UserToken = op;
			args.Completed += (s, e) =>
			                  	{
			                  		var error = e.SocketError;
			                  		Exception exception = null;

									if (error != SocketError.Success)
										exception = new SocketException(e.SocketError);

			                  		((InnerResult) e.UserToken).Complete(exception);
			                  		e.Dispose();
			                  	};
			_socket.SendAsync(args);
		}

		private void EndSendData(IAsyncResult result)
		{
			if (!result.IsCompleted)
				throw new NotImplementedException("Waiting on async. operations is not implemented");

			var op = (InnerResult)result;
			if (op.Exception != null)
				throw op.Exception;
		}

		private void BeginReceiveData(int length, OuterResult finalResult, AsyncCallback callback)
		{
			if (!_socket.Connected)
				throw new SocketException(SocketError.HostDown);

			var buffer = new byte[length];
			var op = new InnerResult(callback, finalResult, buffer);

			var args = new SocketAsyncEventArgs();
			args.SetBuffer(buffer, 0, buffer.Length);
			args.UserToken = op;
			args.Completed += (s, e) =>
			                  	{
			                  		var error = e.SocketError;
			                  		var tmp = (InnerResult) e.UserToken;
			                  		Exception exception = null;

									// This part has to be explained: A pending read operation returns
									// when the connection is closed, but with NO error. However I would expect it
									// to be and the given enum is the closest to resemble the problem IMO.
									if (error == SocketError.Success)
									{
										if (e.BytesTransferred != tmp.Buffer.Length)
											exception = new ConnectionClosedException();
									}
									else
									{
										exception = new SocketException(error);
									}

									tmp.Complete(exception);

			                  		e.Dispose();
			                  	};
			_socket.ReceiveAsync(args);
		}

		private byte[] EndReceiveData(IAsyncResult result)
		{
			if (!result.IsCompleted)
				throw new NotImplementedException("Waiting on async. operations is not implemented");

			var op = (InnerResult)result;
			if (op.Exception != null)
				throw op.Exception;

			return op.Buffer;
		}

		/// <summary>
		/// Serializes the given data into a byte-array and serializes its length into a byte-array as well.
		/// </summary>
		/// <param name="data">The data to serialize</param>
		/// <param name="serializedData">The data, serialized into a byte-array</param>
		/// <param name="serializedLength">The length of the serialized data as a byte-array</param>
		private void Serialize(Data data, out byte[] serializedData, out byte[] serializedLength)
		{
			serializedData = Packet.Serialize(data);

			// TODO: Find unsafe method on server side (using some form of reinterpret cast)
			var buffer = new byte[4];
			var ms = new MemoryStream(buffer);
			var writer = new BinaryWriter(ms);
			writer.Write(serializedData.Length);
			serializedLength = ms.ToArray();
		}

		private int ReadLength(byte[] buffer)
		{
			if (buffer.Length != 4)
				throw new ArgumentException();

			var ms = new MemoryStream(buffer);
			var reader = new BinaryReader(ms);
			return reader.ReadInt32();
		}
	}
}